Skip to content

feat(cmd): add --json machine-readable output for all commands - #41

Merged
willkg merged 2 commits into
mainfrom
json-output
Jul 23, 2026
Merged

feat(cmd): add --json machine-readable output for all commands#41
willkg merged 2 commits into
mainfrom
json-output

Conversation

@willkg

@willkg willkg commented Jul 23, 2026

Copy link
Copy Markdown
Member

Closes #12.

Adds a persistent --json flag that makes every subcommand (info, read, update, create, fix) emit a single structured JSON document to stdout instead of the human output, for scripting and CI. Design was worked out in _plans/015_json-output.md.

Output shape

A stable, versioned envelope for every command:

{
  "schema_version": 1,
  "markfluence_version": "1.4.0",
  "command": "update",
  "results": [ { "ok": true, "status": "published", "file": "docs/foo.md", "page_id": "123", "version": { "previous": 3, "new": 4 }, "page_width": { "value": "max", "default": false }, "attachments": [], "warnings": [], "broken": [], "error": null, "code": null } ],
  "summary": { "total": 1, "succeeded": 1, "failed": 0, "skipped": 0 }
}
  • Per-command stable schema — each command always emits the same keys/shapes (empty → null/[]); the key set differs per command. schema_version bumps on breaking changes.
  • Status verbs per command: published/skipped, created/not_created, changed/consistent, plus failed. info/read carry data only.
  • Compound values are nested objects (version, page_width, author stamps), never display strings.
  • create's two-phase abort lists every input file (failed ones with an error, the rest not_created) and sets summary.aborted: true.
  • Warnings / broken image links are data (arrays on each result), not stderr log lines.

Errors & exit codes

  • Per-file operational failures → results entries {ok:false, error, code}, exit 1.
  • Fatal/pre-flight failures (bad flags, credential resolution) → typed error object on stderr, exit 2.
  • Codes: CONFIG, AUTH, NOT_FOUND, VALIDATION, CONVERT, IO, NETWORK, API.

Implementation

  • New internal/jsonout package: envelope, typed error object, code vocabulary + HTTPError mapping, shared value types.
  • ui.SetJSON silences the human helpers so stdout stays pure JSON; ui.ErrSilent now carries an exit code so the root can exit 0/1/2.
  • Each command's per-file worker refactored to build a typed result struct consumed by both a human renderer and the JSON collector (single source of truth).

Notable decisions made during implementation

  • info/read omit a file key (they take an id/URL ARG, not a FILE) — file lives only on the batch commands.
  • Exit code 2 for config/usage/bad-flag failures applies in both modes (previously such failures exited 1) — more correct, but a human-mode behavior change worth noting.

Tests

  • internal/jsonout unit tests + per-command golden JSON tests built from hand-constructed result structs (no network mock needed).
  • make test && make lint && make vet all green; smoke-tested end-to-end against a mock Confluence server.

willkg added 2 commits July 23, 2026 12:10
Add a persistent --json flag that makes every subcommand (info, read,
update, create, fix) emit a single structured JSON document to stdout
instead of the human output, for scripting and CI. Closes #12.

Output is a stable, versioned envelope: {schema_version,
markfluence_version, command, results, summary}. results holds one
object per target (a single element for the single-target info/read);
each command's result keys are per-command stable, with compound values
(version, page_width, author stamps) as nested objects rather than
display strings.

Per-file operational failures are carried in results as {ok:false,
error, code} and exit 1; fatal/pre-flight failures (bad flags,
credential resolution) emit a typed error object to stderr and exit 2.
Warnings and broken image/link notices become data (warnings/broken
arrays) instead of stderr log lines.

A new internal/jsonout package holds the envelope, the typed error
object, the error-code vocabulary, and the shared value types. Each
command's per-file worker was refactored to build a typed result struct
consumed by both a human renderer and the JSON collector.
Add schema/json-output/v1.json (JSON Schema draft 2020-12) describing the
full --json contract: the envelope root, per-command results/summary
shapes selected via if/then on `command`, and the stderr error object at
#/$defs/errorObject. Every object uses additionalProperties:false.

Add internal/schematest, a test-only helper (using
santhosh-tekuri/jsonschema/v6) that compiles the schema once; each
command's TestSchemaConformance validates the command's actual marshaled
output against it, so the schema cannot drift from the Go structs. Link
the schema from the README.
@willkg

willkg commented Jul 23, 2026

Copy link
Copy Markdown
Member Author

Added a published JSON Schema for the output contract, per discussion:

  • schema/json-output/v1.json (draft 2020-12) — the envelope root, per-command results/summary shapes selected via if/then on command, and the stderr error object at #/$defs/errorObject. Every object uses additionalProperties: false.
  • internal/schematest — a test-only helper (santhosh-tekuri/jsonschema/v6) that compiles the schema once; each command's TestSchemaConformance validates the command's actual marshaled output against it. Because of additionalProperties: false, any new struct field fails the test until the schema is updated — so the schema can't silently drift from the implementation (verified by temporarily breaking the schema and watching the test fail).
  • README links to the schema.

Note: this adds a test-only dependency (santhosh-tekuri/jsonschema/v6, plus its golang.org/x/text and dlclark/regexp2 transitives). They're reachable only from _test.go, so they're not compiled into the shipped binary — but they do land in go.mod/go.sum. Happy to drop to a lighter structural check if you'd rather not carry the dep.

@willkg
willkg merged commit 73692dc into main Jul 23, 2026
1 check passed
@willkg
willkg deleted the json-output branch July 23, 2026 19:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add --json output for commands

1 participant